Ana içeriğe geç

Veri Tipleri

Rust içerisinde birçok veri tipi vardır ve bu veri tipleri kendi aralarında büyük farklılık göstermektedir. Veri tipleri tanımlarken değişken ismimizden sonra değerini belirtmeden önce :Tip şeklinde bir tanımlama yapmamız gerekmektedir. Alt kısımlardaki veri tiplerinde bu yapının neyi ifade ettiğini görüntüleyebilirsiniz.

-> Skalel Veri Tipleri

Skalel tipler tekil verileri ifade etmektedir. 4 ana başlık içerisinde incelenmektedirler. Bunlar:

  • Integer (Tam sayılar)
  • Flooting Point (Ondalıklı sayılar)
  • Booleans (Mantıksal Değer)
  • Characters (Karakterler)

1) Integer (Tam Sayı)

İlk olarak Integer değerleri ile başlayacağız. Integer değerler kendi aralarında içerilerinde tuttukları verinin büyüklüğüne göre sınıflandırılırlar. Bu sınıflandırma şu şekildedir:

  • 8 Bit integer değer (i8)
  • 16 Bit integer değer (i16)
  • 32 Bit integer değer (i32)
  • 64 Bit integer değer (i64)
  • 64 Bit integer değer (i128)

Bir integer tanımlarken altta ifade edilen ifadeyi kullanmamız yeterli olacaktır.

let x: i8 = 10;

Yukarıda ifade edildiği şekilde tanımladığımız değişken 8 bit yer kaplayacak ve 10 değerine sahip olacaktır.

Bu integer değerleri işaretli olabildikleri gibi işaretsiz de olabilirler. Bu ne demek? Eğer bir integer değeri tanımlarken bu değerin + - gibi işaretlere sahip olmasını istemiyorsak unsigned yani işaretsiz integer değeri kullanabiliriz. Ancak eğer değerimiz negatifse ve bu şekilde kullanmak istiyorsak signed integer değerleri kullanabiliriz.

Signed integer değerlerimizi normalde kullandığımız i8 vb. şekilde tanımlarken işaretsiz yani unsigned değerleri u anahtar kelimesi ile tanımlamaktayız. Bunlar:

  • 8 Bit işaretli integer değer (u8)
  • 16 Bit işaretli integer değer (u16)
  • 32 Bit işaretli integer değer (u32)
  • 64 Bit işaretli integer değer (u64)
  • 64 Bit işaretli integer değer (u128)

Bir işaretsiz integer değerini de benzer şekilde tanımlamamız mümkündür:

let y: u8 = 32;

Bu şekilde 8 bit büyüklüğünde işaretsiz 32 değeri tutan bir y değişkeni tanımlamış oluruz.

Şimdi de özelleşmiş tam sayı türleri hakkında konuşabiliriz. Rust içerisinde direkt olarak binary, hex , octal ve decimal sayıları da tanımlayabiliriz. Bunları sırasıyla altta ifade edildiği şekilde tanımlayabiliriz.

  • binary: let binary = 0b1111_1111
  • hex: let hex = 0xff
  • octal: let octal = 0o377
  • binary: let decimal = 02_55

Üstte gösterilen tanımlarda değişken isimlerinin önemli olmadığına ve değerin başında kullanılan işaretli 2 karakterin integerın tipini ifade ettiğini görüntüleyebiliriz.

Not: Rust içerisinde // yapısı ile istemediğimiz kod satılarının çalışmasını kapatabiliriz. Buna yorum satırı denmektedir.

2) Flooting Point (Ondalıklı sayılar)

Ondalıklı sayılar da tanımlanırken integer değerlere benzer şekilde çalışmaktadırlar. İlk olarak tipini özel olarak tanımlamadan oluşturduğumuz değişkenlere bakacak olursak:

let x = 2.0;

Üstte ifade edildiği şekilde tip kullanmadan tanımladığımız değişkenlerde ondalıklı sayılara otomatik olarak bir tip tanımlanmaktadır. Bu tip f64 olmaktadır.

Integerlara benzer şekilde ondalık sayılarda birden fazla türde tanımlanabilir. Bunlar:

  • 32 bit büyüklüğe sahip flooting pointle (f32)
  • 64 bit büyüklüğe sahip flooting pointle (f64)

Flootlar IEEE-754 standartlarını takip etmektedirler.

let x:f32 = 20.0
let x:f64 = 20.1

3) Booleans (Mantıksal Değer)

Boolean değerler Rust ve diğer diller içerisinde mantıksal değerleri ifade etmektedir. Sadece True ve False değerlerini içerebilirler. Kendi içerisinde farkli şekillerde yer alamayacağı için özellikle tip belirmenin gereği olmamaktadır. Küçük harfler ile yazılırlar.

let t = true;
let f:bool = false;

4) Characters (Karakterler)

Rust içerisinde karakterler diğer dillerde olduğu gibi tanımlanabilirler.

let c = 'c';

Aslında yanlış isimlendirilmişlerdir. Char kısaltması ile tanımlıyor olsak da unicode tek bir değeri ifade etmektedir. BU değer bizim alfabemizden farklı ülkelerde kullanılan farklı alfabe yapılarından emojilerden kanjilere neredeyse herşeye atanabilir.

Characterler her zaman 4 byte olmaktadır. Tek tire ile tanımlanmaktadırlar ve genelde işlevsizlerdir. UTF-8 olmamalarından dolayı genelde yerlerine string yapıları kullanılmaktadır.

## 5) Aritmetik Operatörler

Çoğu kodlama dilinde olduğu gibi Rust'da aritmetik işlemleri desteklemektedir. Bu işlemler şu karakterler ile yapmak mümkündür:

  • + toplama yapmaktadır.
  • - çıkarma yapmaktadır.
  • * çarpma yapmaktadır.
  • / bölme yapmaktadır.
  • % kalan bulmaktadır.

-> Compound Veri Tipleri

Compound veri tipleri içerisinde birden çok veriyi taşıyabilen veri tiplerine verilen isim olmaktadır. Rust 2 adet Compound veri tipine sahiptir. Bunlar:

  • Tuples (Demetler)
  • Arrays (Diziler)

6) Tuples (Demetler)

Tuple'lar aynı veya farklı türden birden fazla veriyi gruplandırarak içerisinde tutan compound veri tipi olmaktadır. Parantezler ile () tanımlanmaktadırlar.

Tuple'ların belirli bir büyüklükleri vardır. Tanımlandıktan sonra büyüyemez veya küçülemezler. Şu şekilde tanımlanırlar:

let tup = (500, "hi", true);

Yukarıda görüntülendiği üzere tip eklenmeden tanımlanabilirler. Eğer tipler ile tanıtmak istiyorsak şu şekilde bir tanım yapabiliriz:

let tup :(u8, String, bool) = (500, "hi", true);

Tuple içerisindeki verilere . indexleri ile erişebiliriz. Önce tuple'ımızın adını yazarız, nokta koyarız ve son olarak 0'dan başlayarak hangi index numarasına sahip elemana erişeceğimizin sayısını gireriz. Örneğin tuple içerisindeki "hi" değerine erişmek istiyorsak sıfırdan başlayarak sayarız ve 1 indexsine sahip olduğunu saptarız. Sonrasında altta bulunan kod ile bunu farklı bir değişkene atayabilir veya istediğimiz kod bloğu içerisinde kullanabiliriz.

let merhaba = tup.1;

Bu tuple içerisindeki verilere erişmenin tek yolu olmamaktadır. Eğer bir tuple içerisindeki her veriye erişmek ve her birini bir değişkene atamak istiyorsak şunları yapabiliriz:

let (x,y,z) = tup;

Bu şekilde sırasıyla index numaralarındaki verilerimizi x,y ve z değerlerine eşitleyebiliriz.

Aynı zamanda bu değişkenlerin her birinin tiplerinin farklı olmasını sağlayabiliriz.

let (x:i32,y:&str,z:bool) = tup;

7) Arrayler (Diziler)

Arrayler tuple'lar gibi içerilerinde birden fazla veri tutmamızı sağlayan compound veri tipleridir. Arraylerinde tuple'lar gibi sabit bir uzunluğu olmaktadır. Ancak Tuple'ların aksine arraylerin içerisinde yer alan her bir element aynı tipte olmalıdır. Birden fazla tanım yöntemi bulunmaktadır. Köşeli parantez [] ile tanımlanmaktadırlar.

let dizim = [4,5,6];

Tupler'lar gibi içerisindeki elemanlara erişirken 0'dan başlayarak saymamız gerekmektedir. Arraylerin içerisindeki bir veriye erişmek istiyorsak bu sefer . yerine [] yapısı kullandığımızı görüntüleyebiliriz. Eğer dizim olarak tanımladığımız arrayin 5 verisine erişmek istiyorsak arrayimizin isminni yanına 0'dan başlayarak sayarak bulduğumuz 1 değerini köşeli parantez içerisinde yazmamız yeterlidir.

let bes = dizim[1] // 5

Rust içerisinde arrayleri farklı şekillerde oluşturmak da mümkündür. Örneği belirli bir uzunluğa sahip belirli türden bir array oluşturmak istiyorsak tip kısmına köşeli parantezler ile türü ve bu arrayin uzunluğunu yazabiliriz.

let array2 : [i32;3] = [7,8,9];

Arraylerin tuple'lar gibi kesin bir sabitliği bulunmamaktadır. Bu nedenle uygun düzenlemeler ile içerilerindeki veriler değiştirilebilir. Bu işlem için array'imizi mutable yapmamız gerekmektedir.

let mut array2 : [i32;3] = [7,8,9];
array2[2] = 10; // Artık sonuncu eleman 9 değil 10

Peki arrayimizin içerisinden listeden daha büyük bir değere erişmek istersek yani mesala 3 elemanlı yukarıdaki listede 4. elemana ulaşmaya çalışırsak ne olur? Bu işlem gerçekleştiğinde Rust compiler olan Cargo bize hata oluşturacak ve panic hatası verecektir. Bu durum bizler için çok iyidir çünkü bu hataları kod içerisinde çözmemiz mümkündür. Bu adım hafıza güvenliği için hayati olmaktadır ve sonraki kısımlarda incelenecektir.

8) Vektörler

Vektörler yeninden boyutlandırılabilen element dizilerilerdir. Normal arraylerin boyutları değiştirilemezken bu arraylerin tipleri değiştirilebilir. Heap üzerinde depolanmaktadırlar.

NOT: Heap Nedir?

Vektörler rust üzerinde büyük bir öneme sahiptir. Bu durumun nedeni değişken bir uzunluğa sahip liste oluşturmak istediğimizde vektörleri kullanmamız gerekmesidir.

Vectörlerin en kolay tanımlanma yöntemi VEC Macro (Makrolardan ileriki derslerde bahsedeceğiz) olmaktadır. Bu makroyu şu şekilde tanımlayabiliriz:

let mut nums = vec![1,2,3];

Tanımladığımız bu vectore bahsettiğimiz gibi veri eklemek mümkündür. Bu işlemi diğer kodlama dillerinde de aşina olabileceğiniz şekilde push yöntemi ile yapmaktayız. Vektörümüzü bir değer ekleyerek değiştireceğimizden dolayı mutable bir vektör olmasına dikkat etmeliyiz.

nums.push(4);

Vektörlerimizi görüntülemek için ekrana println! makrosu ile yazdımak istediğimizde bu işlemin bir hata ile karşılaşacağını görüntüleyebiliriz.

println!("{}",nums); //error

Bu hatayı çözmek için işlemi de debug modunda çalıştırmamız mümkündür. Bu moda süslü parantez içerisinde :? ile geçmek mümkündür.

println!("{:?}",nums); //[1, 2, 3, 4]

Bu vektörlere veri ekleyebildiğimiz gibi çıkarmamız da mümkündür. Bu çıkarma işlemini yapmamın birçok yöntemi bulunmaktadır.

Yöntemlerden biri .pop() metodu olmaktadır. Bu method ile vektörümüzün son elamanını listeden çıkarmak mümkün olmaktadır.

numps.pop();
println!("{:?}",nums); //[1, 2, 3]

Rust içerisinde vektör oluşturmanın farklı yolları da bulunmaktadır. Bunlardan biri de Vec::new() olmaktadır. Aslında daha önceden gördüğümüz VEC Macro ifadesini çağırdığımızda Rust arka planda Vec::new() yapısını kullanmaktadır. Bu şekilde oluşturduğumuz vektörlere de aynı şekilde tüm işlemleri yapabiliriz.

let mut vec = Vec::new();
vec.push("Test");
vec.push("String")

Bu şekilde oluşturduğumuz vektörlerde başlangıç değeri atamak tanım içerisinde direkt olarak mümkün olmamaktadır.

Rust içerisinde Vektörler ile ilgili birçok önceden bizim kullanmamız için tasarlanmış methot bulunmaktadır. Bunlardan bazıları:

  • .reverse() vektörü ters çevirmektedir.
  • .push() vektöre eleman eklemektedir.
  • .pop() vektörden eleman çıkarmaktadır.

Aynı zamanda üstte verilen şekli ile rust içerisinde belirli boyutlara sahip vektör oluşturmak da mümkündür. Bu işlemi altta ifade edilen kod ile gerçekleştirebiliriz.

let mut vectorum = Vec::<i32>::with_capacity(2);

Bu şekilde oluşturduğumuz 2 elemanlı vektörün sonraında büyüklüğünü de vektörler içerisinde gelen bir diğer methot olan .capacity() işlemi ile öğrenebiliriz.

println!("{}", vect.capacity()) // 2

Peki bu işlemi yapması sonucunda eğer bu vektöre 2'den fazla eleman eklemek istersek bunu gerçekleştirebilecek mi? Cevap evet olmakta. Boyutundan büyük bir veri eklemeye çalıştığımızda Rust bunu anlayacak eski vektör elemanlarını kopyalayacak hafızada yeni bir alan açarak eski ve yeni verileri uygun boyutla ekleyecek son olarak da eski veriyi silecektir.

Aynı zamanda vektörler tekrar eden veriler (Bunlardan ileride bahsedeceğiz) ile de tanımlanabilinir. Örneğin [0,5) aralığını tanımlamak için rust içerisinde şu şekilde bir işlem yapabiliriz:

let v: Vec<i32> = (0..5).collect();

9) Slices (Kesitler)

Kesitler, herhangi bir arrayin veya vektörün belirli bölgesini ifade edebilecek bağlantılar yani linkler olmaktadır.

Kesitler direkt olarak değişkenler üzerinde tutulamaz veya fonksiyon girdisi olarak verilemez. Hadi ilk slice değerimizi oluşturarak başlayalım:

let v: Vec<i32> = (0..5).collect(); // 0'dan 5'e kadar bir vektör
let sv: &[i32]= &v;

Başlangıç olarak dikkat etmemiz gereken şey yukarıda kullanılan & ifadesidir. Bu ifade referans anlamına gelmektedir. Kurallarımızda gördüğümüz üzere de kesitlerin direkt olarak değişkene tanımlanamamasından dolayı referanslar kullanılır. Bu şekilde veriyi direkt olarak değişkenin içerisinde tutmaktansa verinin hafızada tutulduğu yerin başını işaret etmekteyiz.

Bu işaret etmeye fat pointer ismi verilmektedir. Bu işaret 2 adet veri içermektedir. İlk olarak hafızada referans edilen verinin nerede başladığına, ikinci olarak da bu verinin uzunluğunun ne olduğunu belirtmektedir.

Bu referans ile normalde yapabileceğimiz vektör ve array işlemlerinin çoğunu yapabiliriz ancak hafızadan yeniden yer talep etemeden bu işlemi yapmış oluruz.

let v: Vec<i32> = (0..5).collect(); // 0'dan 5'e kadar bir vektör
println!("{:?}",v); // [0,1,2,3,4]
let sv: &[i32]= &v;
println!("{:?}",sv); // [0,1,2,3,4]
let v: Vec<i32> = (0..5).collect(); // 0'dan 5'e kadar bir vektör
println!("{:?}",v); // [0,1,2,3,4]
let sv: &[i32]= &v[2..4];
println!("{:?}",sv); // [2,3]

NOT: Referanslardan biraz daha bahsetmek gerekirse normal referans ordinary referance tekil bir değere işaret eden sahipsiz pointer olmaktayken kesitlerde kullanılan referanslar ise tekil bir değer yerine belirli bir aralığa işaret eden sahipsiz pointerlara denk gelmektedir.

10) Stringler

  Stringler bitlerden oluşan vektör olarak depolanmalarından dolayı vektörlere büyük ölçüde benzemektedir.

Bu iki kavramın arasındaki fark her zaman UTF-8 standartında değer olmalarıdır. String değerleri heap üzerinde tutulmaktadır. String tanımlamanın birçok yöntemi vardır. Bu yöntemlerden biri String paketini kullanmaktır.

let isim = String::from("Aybars");

Sting değerleri üretmenin farklı bir yolu da .to_string(); yapısını kullanmaktır. Bu işlemi altta ifade edildiği şekilde gerçekleştirebiliriz

let soyisim = "Ayan".to_string();

Bu iki tanımla tanımlanan string değerler aynı olmaktadır. İkiside heap üzerinde tutulmaktadır.

Vektörlere benzer şekilde String'leri de manüpüle edebiliriz. Bu işlemi yaparken birçok yöntemimiz vardır ancak örnek olarak bir değeri farklı bir değerle değiştirmek istiyorsak alt kısımda ifade edildiği gibi bir işlem yapabiliriz.

let yeni_isim = isim.replace("Aybars","Göktuğ")

11) &str Değerleri

String slice veya stir olarak da adlandırılan &str değerleri slice ve vector gibi bir değişkenden text değerini barrow yani ödünç alacaktır. Diğer slice değerleri gibi 2 adet bilgi içerecektir.

String slice değerleri düzenlenemez. String slice değerlerini altta ifade edildiği şekilde tanımlayabiliriz:

let str1 = "merhaba";

String slice değerlerinin nerelerde stringlerin yerlerine kullanacağımızı merak ediyor olabilirsiniz. Şimdilik string slice değerlerinin herhangi bir string değerini referans edeceğini bilmemiz yeterli olacaktır. Bu nedenle string veya string slice değerlerinin ikisinin de kullanılabileceği yerlerde &str tipinin heap üzerinde veri depolamaması nedeniyle tercih edilen olmaktadır.

Ayrıca String Slice değerleri ve Stringler arasında değişiklik yapabiliriz. Örneğin bir string slice değerini string haline getirmek istiyorsak to_string() yapısını kullanmamız yeterli olmaktadır.

let str1 = "hello";
let str2 = str1.to_string();

Aynı şekilde eğer bir string değerini string slice değerini çevirirken değerin başına & ifadesi koymamız yeterli olacaktır.

let str3 = &str2;

Vektörlerde olduğu gibi stringlerde birçok method içermektedir. Bunlardan bağzılarına bakılacak olunursa:

  • == İki string değeri birbirine eşitse true değeri döndürmektedir..
  • != İki string değeri birbirine eşit değilse true değeri döndürmektedir.

12) String Literals

Daha önce de bahsettiğimiz üzere stringler ve string slices değerleri her zaman UTF-8 standartlarına uyan halde olacaktır. Ancak bazen bu standarta kesin olarak uymak istememekteyiz. Bu durumlarda string literal değerlerini kullanabiliriz. Örneğin encoded bir mesaj yazmak istiyorsak bu yapıyı kullanabiliriz.

let rust = "\x52\x75\x73\x74";
println!("{}", rust); // Rust

Özet olarak UTF-8'e uygun metin yazmak istemediğimizde bu veri tipini kullanabiliriz.